package license

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"time"

	"github.com/jmoiron/sqlx"
	"github.com/mattn/go-sqlite3"

	"github.com/SigNoz/signoz/ee/query-service/model"
	basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
	"github.com/SigNoz/signoz/pkg/sqlstore"
	"github.com/SigNoz/signoz/pkg/types"
	"go.uber.org/zap"
)

// Repo is license repo. stores license keys in a secured DB
type Repo struct {
	db    *sqlx.DB
	store sqlstore.SQLStore
}

// NewLicenseRepo initiates a new license repo
func NewLicenseRepo(db *sqlx.DB, store sqlstore.SQLStore) Repo {
	return Repo{
		db:    db,
		store: store,
	}
}

func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
	licensesData := []model.LicenseDB{}
	licenseV3Data := []*model.LicenseV3{}

	query := "SELECT id,key,data FROM licenses_v3"

	err := r.db.Select(&licensesData, query)
	if err != nil {
		return nil, fmt.Errorf("failed to get licenses from db: %v", err)
	}

	for _, l := range licensesData {
		var licenseData map[string]interface{}
		err := json.Unmarshal([]byte(l.Data), &licenseData)
		if err != nil {
			return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
		}

		license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
		if err != nil {
			return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
		}
		licenseV3Data = append(licenseV3Data, license)
	}

	return licenseV3Data, nil
}

// GetActiveLicense fetches the latest active license from DB.
// If the license is not present, expect a nil license and a nil error in the output.
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
	activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
	if err != nil {
		return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
	}

	if activeLicenseV3 == nil {
		return nil, nil
	}
	activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
	return activeLicenseV2, nil
}

func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {
	var err error
	licenses := []model.LicenseDB{}

	query := "SELECT id,key,data FROM licenses_v3"

	err = r.db.Select(&licenses, query)
	if err != nil {
		return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
	}

	var active *model.LicenseV3
	for _, l := range licenses {
		var licenseData map[string]interface{}
		err := json.Unmarshal([]byte(l.Data), &licenseData)
		if err != nil {
			return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
		}

		license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
		if err != nil {
			return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
		}

		if active == nil &&
			(license.ValidFrom != 0) &&
			(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
			active = license
		}
		if active != nil &&
			license.ValidFrom > active.ValidFrom &&
			(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
			active = license
		}
	}

	return active, nil
}

// InsertLicenseV3 inserts a new license v3 in db
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {

	query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`

	// licsense is the entity of zeus so putting the entire license here without defining schema
	licenseData, err := json.Marshal(l.Data)
	if err != nil {
		return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
	}

	_, err = r.db.ExecContext(ctx,
		query,
		l.ID,
		l.Key,
		string(licenseData),
	)

	if err != nil {
		if sqliteErr, ok := err.(sqlite3.Error); ok {
			if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
				zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
				return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
			}
		}
		zap.L().Error("error in inserting license data: ", zap.Error(err))
		return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
	}

	return nil
}

// UpdateLicenseV3 updates a new license v3 in db
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {

	// the key and id for the license can't change so only update the data here!
	query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`

	license, err := json.Marshal(l.Data)
	if err != nil {
		return fmt.Errorf("insert license failed: license marshal error")
	}
	_, err = r.db.ExecContext(ctx,
		query,
		license,
		l.ID,
	)

	if err != nil {
		zap.L().Error("error in updating license data: ", zap.Error(err))
		return fmt.Errorf("failed to update license in db: %v", err)
	}

	return nil
}

func (r *Repo) CreateFeature(req *types.FeatureStatus) *basemodel.ApiError {

	_, err := r.store.BunDB().NewInsert().
		Model(req).
		Exec(context.Background())
	if err != nil {
		return &basemodel.ApiError{Typ: basemodel.ErrorInternal, Err: err}
	}
	return nil
}

func (r *Repo) GetFeature(featureName string) (types.FeatureStatus, error) {
	var feature types.FeatureStatus

	err := r.store.BunDB().NewSelect().
		Model(&feature).
		Where("name = ?", featureName).
		Scan(context.Background())

	if err != nil {
		return feature, err
	}
	if feature.Name == "" {
		return feature, basemodel.ErrFeatureUnavailable{Key: featureName}
	}
	return feature, nil
}

func (r *Repo) GetAllFeatures() ([]basemodel.Feature, error) {

	var feature []basemodel.Feature

	err := r.db.Select(&feature,
		`SELECT * FROM feature_status;`)
	if err != nil {
		return feature, err
	}

	return feature, nil
}

func (r *Repo) UpdateFeature(req types.FeatureStatus) error {

	_, err := r.store.BunDB().NewUpdate().
		Model(&req).
		Where("name = ?", req.Name).
		Exec(context.Background())
	if err != nil {
		return err
	}
	return nil
}

func (r *Repo) InitFeatures(req []types.FeatureStatus) error {
	// get a feature by name, if it doesn't exist, create it. If it does exist, update it.
	for _, feature := range req {
		currentFeature, err := r.GetFeature(feature.Name)
		if err != nil && err == sql.ErrNoRows {
			err := r.CreateFeature(&feature)
			if err != nil {
				return err
			}
			continue
		} else if err != nil {
			return err
		}
		feature.Usage = int(currentFeature.Usage)
		if feature.Usage >= feature.UsageLimit && feature.UsageLimit != -1 {
			feature.Active = false
		}
		err = r.UpdateFeature(feature)
		if err != nil {
			return err
		}
	}
	return nil
}
